#!/usr/bin/env node

"use strict";

var tape = require("blue-tape");

var testhelp = require("./lib/testhelp");

global.Zmodem = require("./lib/zmodem");

var zdle = new Zmodem.ZDLE({ escape_ctrl_chars: true });

tape("trim_leading_garbage", function (t) {
    var header = Zmodem.Header.build("ZACK");

    var header_octets = new Map([
        ["hex", header.to_hex()],
        ["b16", header.to_binary16(zdle)],
        ["b32", header.to_binary32(zdle)],
    ]);

    var leading_garbage = [
        "",
        " ",
        "\n\n",
        "\r\n\r\n",
        "*",
        "**",
        "*\x18",
        "*\x18D",
        "**\x18",
    ];

    leading_garbage.forEach((garbage) => {
        let garbage_json = JSON.stringify(garbage);
        let garbage_octets = testhelp.string_to_octets(garbage);

        for (let [label, hdr_octets] of header_octets) {
            var input = garbage_octets.slice(0).concat(hdr_octets);
            var trimmed = Zmodem.Header.trim_leading_garbage(input);

            t.deepEquals(
                trimmed,
                garbage_octets,
                `${garbage_json} + ${label}: garbage trimmed`,
            );
            t.deepEquals(input, hdr_octets, `… leaving the header`);
        }
    });

    //----------------------------------------------------------------------

    //input, number of bytes trimmed
    var partial_trims = [
        ["*", 0],
        ["**", 0],
        ["***", 1],
        ["*\x18**", 2],
        ["*\x18*\x18", 2],
        ["*\x18*\x18**", 4],
        ["*\x18*\x18*\x18", 4],
    ];

    partial_trims.forEach((cur) => {
        let [input, trimmed_count] = cur;

        let input_json = JSON.stringify(input);

        let input_octets = testhelp.string_to_octets(input);

        let garbage = Zmodem.Header.trim_leading_garbage(input_octets.slice(0));

        t.deepEquals(
            garbage,
            input_octets.slice(0, trimmed_count),
            `${input_json}: trim first ${trimmed_count} byte(s)`,
        );
    });

    t.end();
});

//Test that we parse a trailing 0x8a, since we ourselves follow the
//documentation and put a plain LF (0x0a).
tape("parse_hex", function (t) {
    var octets = testhelp.string_to_octets("**\x18B0901020304a57f\x0d\x8a");

    var parsed = Zmodem.Header.parse(octets);

    t.is(parsed[1], 16, "CRC size");

    t.is(
        parsed[0].NAME,
        "ZRPOS",
        "parsed NAME",
    );

    t.is(
        parsed[0].TYPENUM,
        9,
        "parsed TYPENUM",
    );

    t.is(
        parsed[0].get_offset(),
        0x04030201, //it’s little-endian
        "parsed offset",
    );

    t.end();
});

tape("round-trip, empty headers", function (t) {
    ["ZRQINIT", "ZSKIP", "ZABORT", "ZFIN", "ZFERR"].forEach((n) => {
        var orig = Zmodem.Header.build(n);

        var hex = orig.to_hex();
        var b16 = orig.to_binary16(zdle);
        var b32 = orig.to_binary32(zdle);

        var rounds = new Map([
            ["to_hex", hex],
            ["to_binary16", b16],
            ["to_binary32", b32],
        ]);

        for (const [enc, h] of rounds) {
            let [parsed, crclen] = Zmodem.Header.parse(h);

            t.is(parsed.NAME, orig.NAME, `${n}, ${enc}: NAME`);
            t.is(parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM`);

            //Here’s where we test the CRC length in the response.
            t.is(
                crclen,
                /32/.test(enc) ? 32 : 16,
                `${n}, ${enc}: CRC length`,
            );
        }
    });

    t.end();
});

tape("round-trip, offset headers", function (t) {
    ["ZRPOS", "ZDATA", "ZEOF"].forEach((n) => {
        var orig = Zmodem.Header.build(n, 12345);

        var hex = orig.to_hex();
        var b16 = orig.to_binary16(zdle);
        var b32 = orig.to_binary32(zdle);

        var rounds = new Map([
            ["to_hex", hex],
            ["to_binary16", b16],
            ["to_binary32", b32],
        ]);

        for (const [enc, h] of rounds) {
            //Here’s where we test that parse() leaves in trailing bytes.
            let extra = [99, 99, 99];
            let bytes_with_extra = h.slice().concat(extra);

            let parsed = Zmodem.Header.parse(bytes_with_extra)[0];

            t.is(parsed.NAME, orig.NAME, `${n}, ${enc}: NAME`);
            t.is(parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM`);
            t.is(
                parsed.get_offset(),
                orig.get_offset(),
                `${n}, ${enc}: get_offset()`,
            );

            let expected = extra.slice(0);
            if (enc === "to_hex") {
                expected.splice(0, 0, Zmodem.ZMLIB.XON);
            }

            t.deepEquals(
                bytes_with_extra,
                expected,
                `${enc}: parse() leaves in trailing bytes`,
            );
        }
    });

    t.end();
});

tape("round-trip, ZSINIT", function (t) {
    var opts = [
        [],
        ["ESCCTL"],
    ];

    opts.forEach((args) => {
        var orig = Zmodem.Header.build("ZSINIT", args);

        var hex = orig.to_hex();
        var b16 = orig.to_binary16(zdle);
        var b32 = orig.to_binary32(zdle);

        var rounds = new Map([
            ["to_hex", hex],
            ["to_binary16", b16],
            ["to_binary32", b32],
        ]);

        var args_str = JSON.stringify(args);

        for (const [enc, h] of rounds) {
            let parsed = Zmodem.Header.parse(h)[0];

            t.is(parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME`);
            t.is(
                parsed.TYPENUM,
                orig.TYPENUM,
                `opts ${args_str}: ${enc}: TYPENUM`,
            );

            t.is(
                parsed.escape_ctrl_chars(),
                orig.escape_ctrl_chars(),
                `opts ${args_str}: ${enc}: escape_ctrl_chars()`,
            );
            t.is(
                parsed.escape_8th_bit(),
                orig.escape_8th_bit(),
                `opts ${args_str}: ${enc}: escape_8th_bit()`,
            );
        }
    });

    t.end();
});

tape("round-trip, ZRINIT", function (t) {
    var opts = [];

    [[], ["CANFDX"]].forEach((canfdx) => {
        [[], ["CANOVIO"]].forEach((canovio) => {
            [[], ["CANBRK"]].forEach((canbrk) => {
                [[], ["CANFC32"]].forEach((canfc32) => {
                    [[], ["ESCCTL"]].forEach((escctl) => {
                        opts.push([
                            ...canfdx,
                            ...canovio,
                            ...canbrk,
                            ...canfc32,
                            ...escctl,
                        ]);
                    });
                });
            });
        });
    });

    opts.forEach((args) => {
        var orig = Zmodem.Header.build("ZRINIT", args);

        var hex = orig.to_hex();
        var b16 = orig.to_binary16(zdle);
        var b32 = orig.to_binary32(zdle);

        var rounds = new Map([
            ["to_hex", hex],
            ["to_binary16", b16],
            ["to_binary32", b32],
        ]);

        var args_str = JSON.stringify(args);

        for (const [enc, h] of rounds) {
            let parsed = Zmodem.Header.parse(h)[0];

            t.is(parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME`);
            t.is(
                parsed.TYPENUM,
                orig.TYPENUM,
                `opts ${args_str}: ${enc}: TYPENUM`,
            );

            t.is(
                parsed.can_full_duplex(),
                orig.can_full_duplex(),
                `opts ${args_str}: ${enc}: can_full_duplex()`,
            );
            t.is(
                parsed.can_overlap_io(),
                orig.can_overlap_io(),
                `opts ${args_str}: ${enc}: can_overlap_io()`,
            );
            t.is(
                parsed.can_break(),
                orig.can_break(),
                `opts ${args_str}: ${enc}: can_break()`,
            );
            t.is(
                parsed.can_fcs_32(),
                orig.can_fcs_32(),
                `opts ${args_str}: ${enc}: can_fcs_32()`,
            );
            t.is(
                parsed.escape_ctrl_chars(),
                orig.escape_ctrl_chars(),
                `opts ${args_str}: ${enc}: escape_ctrl_chars()`,
            );
            t.is(
                parsed.escape_8th_bit(),
                orig.escape_8th_bit(),
                `opts ${args_str}: ${enc}: escape_8th_bit()`,
            );
        }
    });

    t.end();
});

tape("hex_final_XON", function (t) {
    var hex_ZFIN = Zmodem.Header.build("ZFIN").to_hex();

    t.notEquals(
        hex_ZFIN.slice(-1)[0],
        Zmodem.ZMLIB.XON,
        "ZFIN hex does NOT end with XON",
    );

    var hex_ZACK = Zmodem.Header.build("ZACK").to_hex();

    t.notEquals(
        hex_ZACK.slice(-1)[0],
        Zmodem.ZMLIB.XON,
        "ZACK hex does NOT end with XON",
    );

    var headers = [
        "ZRQINIT",
        Zmodem.Header.build("ZRINIT", []),
        Zmodem.Header.build("ZSINIT", []),
        "ZRPOS",
        "ZABORT",
        "ZFERR",
    ];

    //These are the only headers we expect to send as hex … right?
    headers.forEach((hdr) => {
        if (typeof hdr === "string") hdr = Zmodem.Header.build(hdr);

        t.is(
            hdr.to_hex().slice(-1)[0],
            Zmodem.ZMLIB.XON,
            `${hdr.NAME} hex ends with XON`,
        );
    });

    t.end();
});
